import { App, Provide, Inject } from '@midwayjs/decorator'
import { CacheManager } from '@midwayjs/cache'

import { Application, Context } from 'egg'
import { gql } from 'graphql-request'

import { AuthService } from './auth'
import { TicketService } from './ticket'
import { getRealIp, hasKeyIn, getCurrentDate } from '../utils/helper'
import { compare } from '../utils/compare'

const bcrypt = require('bcrypt')
const _ = require('lodash')

enum GetQueryKeys {
  USERNAME = 'username',
  PHONE = 'phone',
  EMAIL = 'email',
  ID = 'id'
}

const READONLY = ['username', 'email']
const TOKEN_SENSTIVE = ['password', 'active', 'role.id', 'is_system_user']

export const SystemUserFragment = `
  id
  username
  password
  phone
  email
  active
  last_ip
  last_login_at
  nickname
  avatar
  deleted_at
  create_at
  update_at
  role: ie_system_user_role {
    id
    role
    rolename
  }
  system_user_meta {
    id
    department {
      id
      name
    }
    districts: system_users_districts {
      district {
        id
        name
      }
    }
  }
`

@Provide()
export class SystemUserService {

  @App()
  app: Application

  @Inject()
  ctx: Context

  @Inject()
  cache: CacheManager

  @Inject()
  authService: AuthService

  @Inject()
  ticketService: TicketService

  async get(query: {
    [key in GetQueryKeys]?: string
  }) {
    const where = {}

    Object.keys(query).forEach(key => {
      where[key] = { _eq: query[key] }
    })

    try {
      const { ie_system_user: [system_user] } = await this.app.gqlRequest(gql`
        query ($where: ie_system_user_bool_exp!) {
          ie_system_user (where: $where) {
            ${SystemUserFragment}
          }
        }
      `, { where })

      if (Array.isArray(system_user?.system_user_meta?.districts)) {
        system_user.system_user_meta.districts =
          system_user.system_user_meta.districts.map(({ district }) => {
            return district
          })
      }

      return system_user
    } catch (err) {}
  }

  async add(user) {
    const {
      password,
      department_id,
      district_id = [],
      ...userProps
    } = user

    userProps.password = await bcrypt.hash(password, 10)

    // insert empty system_user_meta
    userProps.system_user_meta = {
      data: {}
    }

    if (department_id) {
      userProps.system_user_meta.data.department_id = department_id
    }

    if (
      Array.isArray(district_id) &&
      district_id.length
    ) {
      userProps.system_user_meta.data.system_users_districts = {
        data: district_id.map(did => ({ district_id: did }))
      }
    }

    try {
      const {
        insert_ie_system_user: {
          returning: [addedUser]
        }
      } = await this.app.gqlRequest(gql`
        mutation ($user: ie_system_user_insert_input!) {
          insert_ie_system_user (objects: [$user]) {
            affected_rows
            returning {
              ${SystemUserFragment}
            }
          }
        }
      `, {
        user: userProps
      })
  
      return addedUser
    } catch (err) {
      this.app.logger.error(err)
    }
  }

  async update(updated, oSystemUser) {
    const safeUpdated = _.omit(updated, READONLY)
    const {
      id,
      password,
      department_id,
      district_id,
      ...user
    } = safeUpdated

    if (password) {
      user.password = await bcrypt.hash(password, 10)
    }

    // marked token changed
    if (hasKeyIn(safeUpdated, TOKEN_SENSTIVE)) {
      await this.cache.set(id, true, { ttl: 7 * 24 * 3600 })
    }

    let departmentQuery = ''
    let districtQuery = ''

    if (department_id) {
      departmentQuery = `
        update_system_user_meta (
          where: { system_user_id: { _eq: "${id}" } }
          _set: { department_id: ${department_id} }
        ) {
          affected_rows
        }
      `
    }

    const {
      system_user_meta = {}
    } = oSystemUser

    if (system_user_meta?.id) {

      if (Array.isArray(district_id)) {
        const {
          _insert,
          _delete
        } = compare<number>(
          (system_user_meta?.districts || []).map(({ id: _id }) => _id),
          _.uniq(district_id.filter(_.isNumber))
        )
  
        if (_insert.length) {
          const _insert_stringify = JSON.stringify(
            _insert.map(did => ({ district_id: did, system_user_meta_id: system_user_meta.id }))
          )
  
          districtQuery += `
            insert_system_users_districts (
              objects: ${_insert_stringify.replace(/"([^(")"]+)":/g, '$1:')}
            ) {
              affected_rows
            }
          `
        }
  
        if (_delete.length) {
          districtQuery += `
            delete_system_users_districts (
              where: { _and: {
                system_user_meta_id: { _eq: "${system_user_meta.id}" }
                district_id: { _in: [${_delete}] }
              } }
            ) {
              affected_rows
            }
          `
        }
      }
    }

    if (id) {
      const {
        update_ie_system_user: {
          returning: [updatedUser]
        }
      } = await this.app.gqlRequest(gql`
        mutation ($id: uuid, $user: ie_system_user_set_input!) {
          update_ie_system_user (
            where: { id: { _eq: $id } }
            _set: $user
          ) {
            returning {
              ${SystemUserFragment}
            }
          }
          ${departmentQuery}
          ${districtQuery}
        }
      `, {
        id,
        user
      })

      return updatedUser
    }
  }

  async validate(password, hashed) {
    return await bcrypt.compare(password, hashed)
  }

  async afterLogin(user, remember_me) {
    const payload = _.pick(user, [
      'id',
      'username',
      'role',
      'active',
      'system_user_meta'
    ])

    payload.is_system_user = true

    if (payload.role?.role) {
      payload.role = payload.role.role
    }

    if (payload.system_user_meta) {
      const department_id = payload?.system_user_meta?.department?.id

      if (department_id) {
        payload.department_id = department_id
      }

      delete payload.system_user_meta
    }

    // add token
    user.token = await this.authService.generateToken(payload)

    const tokenExpired = await this.cache.get(payload.id)

    if (tokenExpired) {
      if (remember_me) {
        user.refresh_token = await this.authService.generateToken(payload, true)
      } else {
        await this.authService.removeRefreshToken(payload.id)
      }
    } else if (remember_me) {
      user.refresh_token =
        await this.authService.getRefreshToken(payload.id) ||
        await this.authService.generateToken(payload, true)
    }

    // add last login ip + time
    const updateProps = {
      last_ip: getRealIp(this.ctx),
      last_login_at: 'now()'
    }

    const { last_login_at } = await this.update({
      id: user.id,
      ...updateProps
    }, user)

    return {
      ...user,
      ...updateProps,
      last_login_at
    }
  }

  async stats(department_id) {
    const THISWEEK = getCurrentDate(0, 'week')
    const LASTWEEK = getCurrentDate(-1, 'week')

    function unwrap(o) {
      if (o?.aggregate?.count !== undefined) {
        return o.aggregate.count
      }

      return o
    }

    try {
      const {
        department_children = []
      } = await this.app.gqlRequest(gql`
        query ($id: Int!) {
          department_children (args: { department_id: $id }) {
            id
          }
        }
      `, { id: department_id })

      const stats: any = await this.app.gqlRequest(gql`
        query ($ids: [Int!]!, $THISWEEK: timestamp!, $LASTWEEK: timestamp!) {
          thisweek_create_count: ticket_aggregate (
            where: { _and: {
              department_id: { _in: $ids }
              create_at: { _gte: $THISWEEK }
            } }
          ) {
            aggregate {
              count
            }
          }
          lastweek_create_count: ticket_aggregate (
            where: { _and: {
              department_id: { _in: $ids }
              create_at: { _lt: $THISWEEK, _gte: $LASTWEEK }
            } }
          ) {
            aggregate {
              count
            }
          }
          thisweek_accept_count: ticket_aggregate (
            where: { _and: {
              department_id: { _in: $ids }
              status: { _gte: ${this.ticketService.STATUS.CANCELLED} }
              accept_at: { _gte: $THISWEEK }
            } }
          ) {
            aggregate {
              count
            }
          }
          lastweek_accept_count: ticket_aggregate (
            where: { _and: {
              department_id: { _in: $ids }
              status: { _gte: ${this.ticketService.STATUS.CANCELLED} }
              accept_at: { _lt: $THISWEEK, _gte: $LASTWEEK }
            } }
          ) {
            aggregate {
              count
            }
          }
          thisweek_process_count: ticket_aggregate (
            where: { _and: {
              department_id: { _in: $ids }
              status: { _gt: ${this.ticketService.STATUS.PROCESSING} }
              process_at: { _gte: $THISWEEK }
            } }
          ) {
            aggregate {
              count
            }
          }
          lastweek_process_count: ticket_aggregate (
            where: { _and: {
              department_id: { _in: $ids }
              status: { _gt: ${this.ticketService.STATUS.PROCESSING} }
              process_at: { _lt: $THISWEEK, _gte: $LASTWEEK }
            } }
          ) {
            aggregate {
              count
            }
          }
        }
      `, {
        ids: department_children.map(({ id }) => id),
        THISWEEK,
        LASTWEEK
      })

      for (let key of Object.keys(stats)) {
        stats[key] = unwrap(stats[key])
      }

      return stats
    } catch (err) {
      this.app.logger.error(err)
    }
  }
}
